/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package nsip.components;

import java.awt.Color;
import nsip.graph.ConstantInfo;
import nsip.graph.GraphInfo;
import nsip.util.History;
import java.util.HashSet;
import java.util.Set;
import java.util.Arrays;
import java.util.Collection;
import java.util.Random;
import java.util.logging.Logger;
import nsip.NsipAgent;
import nsip.components.ds.bi.Metric;
import nsip.components.ds.bi.SimpleValue;
import nsip.components.ds.bi.Sum;
import nsip.graph.GraphWindow;
import nsip.graph.HistorybasedGraphRenderer;
import nsip.model.ProductPriceInfo;
import nsip.transport.NsipTransport;
import nsip.model.RFQSet;
import nsip.model.messages.AcceptedCustomerRFQsBundle;
import nsip.model.messages.CustomerOrderBundle;
import nsip.model.DataRecord;
import nsip.model.Item;
import nsip.model.messages.NewDay;
import nsip.util.Const;
import nsip.util.LazyInitializingMap;
import se.sics.tasim.props.MarketReport;
import se.sics.tasim.props.OfferBundle;

import static nsip.util.Const.Sales.*;
import static nsip.components.ds.StatisticType.*;

/**
 *
 * @author nico.rehwaldt
 */
public class SalesAgent extends NsipAgent {
    private static final int MINIMAL_PROFIT = 300;

    private static final Logger logger = Logger.getLogger("SalesAgent");

    private final Random random = new Random();
    private final Set<Item> issuedOffers = new HashSet();

    private History<SalesDetails> salesHistory;

    private GraphWindow salesGraphWindow;
    
    // Factor to do some magic with
    private final Metric salesDiscount;
    private final History<SalesBounds> salesDiscountBoundsHistory;
    
    public SalesAgent(InformationSystem is,
                      NsipTransport transport) {
        
        super(is, transport);

        salesHistory = new History(new SalesDetails(0, 0, 0));
        salesDiscountBoundsHistory = new History(new SalesBounds(0.5));
        
        salesDiscount = new Sum(SALES_PRICE_STARTING_BOUND);

        statistics().addMetric(RECEIVED_SALES_ORDERS_COUNT, new SimpleValue(0));
        statistics().addMetric(RECEIVED_SALES_ORDERS_TO_OFFERS_RATIO, new SimpleValue(0.15));
        statistics().addMetric(LAST_DAY_CUSTOMER_OFFERS, new SimpleValue(0));
        statistics().addMetric(SALES_PRICE_DISCOUNT_FACTOR, salesDiscount);

        salesGraphWindow = new GraphWindow("Sales Data", 
            new SalesDetailsGraphRenderer(salesHistory),
            new SalesBoundsGraphRenderer(salesDiscountBoundsHistory));
        
        salesGraphWindow.setVisible(true);
    }

    /**
     * Allows the implementation of an agent to reject rfqs which are
     * totally unacceptable for him and thus to reduce the "problem space".
     *
     * @param rfq
     *
     * @return true if the agent accepts the specified rfq, false otherwise
     */
    public boolean acceptsRFQ(Item rfq) {
        ProductPriceInfo info = is().getProductPriceInfo(rfq.getProductID());
        rfq.metadata().setPriceInfo(info);

        // Sales only like rfqs if there is at least a
        // minimal possibility to get profit.
        double partsPrice = getEstimatedPartsPrice(rfq);
        double profit = rfq.getPricePerUnit() - partsPrice;
        
        // Reject if parts price is bigger than 95% of reserve price
        if (profit < MINIMAL_PROFIT) {
            return false;
        }
        
        return true;
    }

    /**
     * Evaluate the specified rfq set and returns a value which gives credit
     * to the suitablity of the rfqset from this agens view point
     *
     * @param rfqSet
     * @return value of rfq set
     */
    public double evaluateRFQSet(RFQSet rfqSet) {
        double score = 0.0;
        int count = 0;
        
        for (Item rfq: rfqSet) {
            int quantity = rfq.getQuantity();
            int daysLeft = rfq.getDueDate() - is().getCurrentDate();
            int requiredCycles = rfq.metadata().getInfo().getRequiredAssemblyCycles();
            int penalty = rfq.getPenalty();
            
            int materialCost = getEstimatedPartsPrice(rfq);
            
            double profit = (rfq.getPricePerUnit() - materialCost) * quantity;
            double profitPerCycle = profit / requiredCycles;
            double scorePerRFQ = (profitPerCycle / 
                    (penalty != 0 ? Math.sqrt(penalty) : penalty) *
                    (daysLeft != 0 ? Math.sqrt(daysLeft) : daysLeft));
            
            score += scorePerRFQ;            
            count++;
        }

        score /= count;
        return score;
    }

    private void sendCustomerOffers(Collection<Item> rfqs) {
        SalesDetails lastSales = salesHistory.last();
        
        int issuedOffersCount = lastSales.issuedOffers;
        int receivedOrdersCount = issuedOffersCount - issuedOffers.size();

        double orderToOfferRatio = (0.0 + receivedOrdersCount) / issuedOffersCount;
        
        statistics()
            .getMetric(RECEIVED_SALES_ORDERS_TO_OFFERS_RATIO)
            .update(orderToOfferRatio);

        SalesBounds offerBounds = salesDiscountBoundsHistory.last().copy();
        
        if (orderToOfferRatio < Const.Sales.DESIRED_ORDER_TO_OFFER_RATIO) {
            offerBounds.update(-(random.nextDouble() / 17));
        } else {
            offerBounds.update(random.nextDouble() / 17);
        }

        // Send new customer offers to the client based on his rfqs
        OfferBundleMap offerMap = new OfferBundleMap();
        
        // Reset last days open offers. We need this map to store todays offers
        issuedOffers.clear();
        
        for (Item rfq: rfqs) {
            // Add rfqs we are going to respond to to database
            is().getCustomerDB().add(rfq);
            
            int reservePrice = rfq.getPricePerUnit();
            int partsPrice = getEstimatedPartsPrice(rfq);
            int possibleProfit = reservePrice - partsPrice;

            System.out.println(offerBounds.bound() + " " + offerBounds.randomCandidate(random));
            
            int offeredPrice = (int) (offerBounds.randomCandidate(random) * possibleProfit + partsPrice);

            Item offer = rfq.copy();
            offer.update(new DataRecord.CustomerOffer(
                rfq.getDueDate(), rfq.getQuantity(),
                offeredPrice));
            
            // Add the response, too
            is().getCustomerDB().add(offer);
            issuedOffers.add(offer);
            
            offerMap.get(rfq.getPeerAddress()).addOffer(
                    offer.getId(), rfq.getId(), offer.getPricePerUnit(),
                    offer.getDueDate(), offer.getQuantity());
        }
        
        logger.fine("Sending offers to customer: " + offerMap.toString());
        
        // Finished adding offers. Send all offers to the customers.
        transport().sendMessages(offerMap);
        
        SalesDetails todaysSales = new SalesDetails(
            rfqs.size(),
            issuedOffers.size(),
            receivedOrdersCount);

        salesHistory.add(todaysSales);
        salesDiscountBoundsHistory.add(offerBounds);

        logger.info("[SalesDetails] " + todaysSales);
        logger.info("[SalesBounds] " + offerBounds);
    }

    @Override
    public void receive(Object message) {
        if (message instanceof MarketReport) {
            // TODO: Process market report
        } else

        if (message instanceof AcceptedCustomerRFQsBundle) {
            sendCustomerOffers(((AcceptedCustomerRFQsBundle) message).getRFQs());
        } else
        
        if (message instanceof CustomerOrderBundle) {
            handleCustomerOrders((CustomerOrderBundle) message);
        } else

        if (message instanceof NewDay) {
            salesGraphWindow.redraw();
            // Plug in some daily postprocessing here
        }
    }

    /**
     * Return parts prices for given product if it would be produced at the
     * specified date.
     * 
     * @param productID
     * @param productionDate
     * @return
     */
    private int getEstimatedPartsPrice(Item item) {
        int price = is().getPriceTracker().getComponentPrices(
                item.getProductID(), is().getCatalog(),
                item.getProductionDueDate() - 2);

        if (price == -1) {
            double supplyDiscountRatio = statistics().getMetric(AVERAGE_SUPPLY_DISCOUNT_RATIO).value();
            price = (int) (item.metadata().getInfo().getPartsPrice() * supplyDiscountRatio);
        }
        
        return price;
    }
    
    @Override
    public Collection<Class> getMessageInterests() {
        return Arrays.asList(new Class[] {
            // ComponentCatalog.class,
            // BOMBundle.class,
            // StartInfo.class,
            // BankStatus.class,
            MarketReport.class,
            AcceptedCustomerRFQsBundle.class,
            // SupplierOfferBundle.class,
            CustomerOrderBundle.class,
            // DeliveryNotice.class,
            // FactoryStatus.class,
            // PriceReport.class,
            NewDay.class
        });
    }

    private void handleCustomerOrders(CustomerOrderBundle orderBundle) {
        for (Item order: orderBundle.getOrders()) {
            issuedOffers.remove(order.getParent());
        }
    }

    private static class OfferBundleMap extends LazyInitializingMap<String, OfferBundle> {

        @Override
        public OfferBundle initialize(Object key) {
            return new OfferBundle();
        }
    };

    /**
     * An item to store sales details for a given amount of time
     */
    private class SalesDetails {
        
        public final int receivedRFQs;
        public final int issuedOffers;
        public final int receivedOrders;

        /**
         * Creates a new sales details item
         * 
         * @param receivedRFQs
         * @param issuedOffers
         * @param receivedOrders
         */
        public SalesDetails(int receivedRFQs, int issuedOffers, int receivedOrders) {
            this.receivedRFQs = receivedRFQs;
            this.issuedOffers = issuedOffers;
            this.receivedOrders = receivedOrders;
        }

        @Override
        public String toString() {
            return "{ rfqs: " + receivedRFQs + ", offers: " +
                   issuedOffers + ", orders: " + receivedOrders + " }";
        }
    }

    private static class SalesDetailsGraphRenderer extends HistorybasedGraphRenderer<SalesDetails> {

        private static GraphInfo[] graphs = new GraphInfo[] {
            new GraphInfo("Issue Offers", Color.BLUE),
            new GraphInfo("Received Orders", Color.RED)
        };
        
        public SalesDetailsGraphRenderer(History<SalesDetails> salesHistory) {
            super(salesHistory, 220, "offers");
        }

        @Override
        protected GraphInfo[] getGraphInfos() {
            return graphs;
        }

        @Override
        protected int[] extractData(SalesDetails item, History<SalesDetails> history) {
            return new int[] {
                history.last().issuedOffers,
                item.receivedOrders
            };
        }

        @Override
        public int[] getGraph(int index) {
            int[] data = super.getGraph(index);
            return data;
        }
    }

    private static class SalesBoundsGraphRenderer extends HistorybasedGraphRenderer<SalesBounds> {

        private static GraphInfo[] graphs = new GraphInfo[] {
            new GraphInfo("Sales bounds", Color.GREEN)
        };

        public SalesBoundsGraphRenderer(History<SalesBounds> history) {
            super(history, 230, "sales bounds");
        }        

        @Override
        protected GraphInfo[] getGraphInfos() {
            return graphs;
        }

        @Override
        public ConstantInfo[] getConstants() {
            return new ConstantInfo[] {
                new ConstantInfo(50, Color.RED)
            };
        }
        
        @Override
        protected int[] extractData(SalesBounds item, History<SalesBounds> history) {
            return new int[] {
                (int) (item.bound())
            };
        }
    }

    private static final class SalesBounds {

        private final static double UPPER = 0.92;
        private final static double LOWER = 0.08;

        private final static double MID = (UPPER + LOWER) / 2;
        private final static double MAX_RANGE = UPPER - LOWER;
        private final static double HALF_MAX_RANGE = MAX_RANGE / 2;
        
        private double bound = 0.0;

        public SalesBounds(double bound) {
            update(bound);
        }
        
        public double bound() {
            return bound;
        }
        
        public void update(double diff) {
            bound += diff;
            
            bound = Math.min(bound, UPPER);
            bound = Math.max(bound, LOWER);
        }

        public double randomCandidate(Random random) {

            // Implements a window which gets bigger around the middle of the
            // spectrum (0.5) and lower on its ends (0.05 / 0.95)
            
            double d = bound - MID;
            double deviation = Math.abs(d);
            double range = (HALF_MAX_RANGE - deviation) * 2;
            
            return bound + (random.nextDouble() * range) - (range / 2);
        }

        public SalesBounds copy() {
            return new SalesBounds(bound);
        }
        
        @Override
        public String toString() {
            return "(" + bound + ")";
        }
    }
}
